import sys
import ure as re
from .threading import Thread, Condition, AsyncTask
from .datetime import DateTime, TimeDelta


class HeapAlgorithm(object):

    @classmethod
    def __siftdown(cls, heap, startpos, pos):
        newitem = heap[pos]
        while pos > startpos:
            parentpos = (pos - 1) >> 1
            parent = heap[parentpos]
            if newitem < parent:
                heap[pos] = parent
                pos = parentpos
                continue
            break
        heap[pos] = newitem

    @classmethod
    def __siftup(cls, heap, pos):
        endpos = len(heap)
        startpos = pos
        newitem = heap[pos]
        childpos = 2 * pos + 1
        while childpos < endpos:
            rightpos = childpos + 1
            if rightpos < endpos and not heap[childpos] < heap[rightpos]:
                childpos = rightpos
            heap[pos] = heap[childpos]
            pos = childpos
            childpos = 2 * pos + 1
        heap[pos] = newitem
        cls.__siftdown(heap, startpos, pos)

    @classmethod
    def heapify(cls, sequence):
        if not isinstance(sequence, list):
            raise TypeError("`sequence` must be instance of list")
        n = len(sequence)
        for i in reversed(range(n // 2)):
            cls.__siftup(sequence, i)

    @classmethod
    def pop(cls, heap):
        lastelt = heap.pop()  # raise IndexError if heap is empty
        if heap:
            returnitem = heap[0]
            heap[0] = lastelt
            cls.__siftup(heap, 0)
            return returnitem
        return lastelt

    @classmethod
    def push(cls, heap, item):
        heap.append(item)
        cls.__siftdown(heap, 0, len(heap) - 1)

    @classmethod
    def poppush(cls, heap, item):
        """more efficient than pop() followed by push()"""
        returnitem = heap[0]  # raise IndexError if heap is empty
        heap[0] = item
        cls.__siftup(heap, 0)
        return returnitem

    @classmethod
    def pushpop(cls, heap, item):
        """more efficient than push() followed by pop()"""
        if heap and heap[0] < item:
            item, heap[0] = heap[0], item
            cls.__siftup(heap, 0)
        return item


class BaseTrigger(object):
    MAX_CALIBRATION_TIMEOUT = 600

    def __init__(self):
        self.next_run_time = None

    def get_remaining_seconds(self):
        # WARN: max remaining is TimeDelta(seconds=0x20C49B)
        value = (self.next_run_time - DateTime.now()).total_seconds()
        # time calibration
        return value if value <= self.MAX_CALIBRATION_TIMEOUT else self.MAX_CALIBRATION_TIMEOUT

    def update(self):
        """overwritten to set self.next_run_time, if valid return True else False"""
        raise NotImplementedError("BaseTrigger.update must be overwritten by subclass")


class IntervalTrigger(BaseTrigger):

    def __init__(self, interval, start_time=None, end_time=None):
        if not isinstance(interval, int):
            raise TypeError("`interval` must be instance of int")
        if not isinstance(start_time, (DateTime, type(None))):
            raise TypeError("`start_time` must be instance of DateTime or None")
        if not isinstance(end_time, (DateTime, type(None))):
            raise TypeError("`end_time` must be instance of DateTime or None")
        self.interval = interval
        self.start_time = start_time
        self.end_time = end_time
        super().__init__()

    def update(self):
        self.next_run_time = DateTime.now() + TimeDelta(seconds=self.interval)
        if self.start_time is not None and self.start_time > self.next_run_time:
            self.next_run_time = self.start_time
        return True if self.end_time is None else self.next_run_time <= self.end_time


class DateTimeTrigger(BaseTrigger):

    def __init__(self, datetime):
        if not isinstance(datetime, DateTime):
            raise TypeError("`datetime` must be instance of DateTime")
        self.datetime = datetime
        super().__init__()

    def update(self):
        self.next_run_time = self.datetime
        return self.next_run_time > DateTime.now()


class CronExpr(object):
    """Cron 表达式是一种用于定义定时任务的格式化字符串。

    它被广泛用于 Unix、Linux 和类 Unix 系统中，用于在指定的时间执行预定的任务。
    本实现 Cron 表达式由5个字段组成，每个字段通过空格分隔开。Cron表达式的格式如下：

    * * * * *
    | | | | | +-- 星期（0-6，周日=0）
    | | | | +---- 月份（1-12）
    | | | +------ 日期（1-31）
    | | +-------- 小时（0-23）
    | +---------- 分钟（0-59）

    *：代表所有可能的值，例如在小时字段中表示每一个小时。
    ?：在日期和星期字段中，表示不指定值。
    -：表示范围，例如在小时字段中 1-3 表示1点到3点。
    ,：表示列举值，例如在小时字段中 1,3,5 表示1点、3点和5点。
    /：表示递增，例如在分钟字段中 */15 表示每15分钟。
    """

    MAX_ITERATIONS = 1000

    def __init__(self, expr):
        self.expr = expr
        # 解析 cron 表达式
        fields = expr.split()
        if len(fields) != 5:
            raise ValueError("Cron 表达式必须有5个字段")
        self.minutes = self.__parse_cron_field(fields[0], 0, 59)
        self.hours = self.__parse_cron_field(fields[1], 0, 23)
        self.days = self.__parse_cron_field(fields[2], 1, 31)
        self.months = self.__parse_cron_field(fields[3], 1, 12)
        self.weekdays = self.__parse_cron_field(fields[4], 0, 6)
    
    def __repr__(self):
        return "CronExpr('{}')".format(self.expr)

    @staticmethod
    def __parse_cron_field(field, min_val, max_val):
        """解析单个 cron 字段"""
        if field == '*':
            return list(range(min_val, max_val + 1))
        
        values = set()
        parts = field.split(',')

        for part in parts:
            # 处理步长 */n 或 a-b/n
            step_match = re.search(r'^(\*|\d+-\d+)/(\d+)$', part)
            if step_match:
                base = step_match.group(1)
                step = int(step_match.group(2))
                
                if base == '*':
                    base_start = min_val
                    base_end = max_val
                else:
                    base_start, base_end = map(int, base.split('-'))
                
                values.update(range(base_start, base_end + 1, step))
                continue
            
            # 处理范围 a-b
            if '-' in part:
                start, end = map(int, part.split('-'))
                values.update(range(start, end + 1))
                continue
            
            # 处理单个值
            values.add(int(part))
        
        return sorted(values)

    def get_next_time(self, from_time=None):
        """计算 cron 表达式的下一次执行时间"""
        if from_time is None:
            from_time = DateTime.now()
    
        # 从当前时间的下一分钟开始查找
        next_time = (from_time + TimeDelta(minutes=1)).replace(second=0)

        for _ in range(self.MAX_ITERATIONS):  # 防止无限循环
            # 检查月份
            if next_time.month not in self.months:
                # 跳到下个月的第一天
                next_month = min([m for m in self.months if m > next_time.month], default=None)
                if next_month is not None:
                    next_time = next_time.replace(
                        month=next_month,
                        day=1,
                        hour=min(self.hours),
                        minute=min(self.minutes)
                    )
                else:
                    next_time = next_time.replace(
                        year=next_time.year + 1,
                        month=min(self.months),
                        day=1,
                        hour=min(self.hours),
                        minute=min(self.minutes)
                    )
                continue
            
            # 检查日期和星期
            day_matched = (next_time.day in self.days)
            weekday_matched = (next_time.weekday in self.weekdays)
            
            if not (day_matched and weekday_matched):
                # 跳到下一天的开始
                next_time = next_time.replace(hour=min(self.hours), minute=min(self.minutes)) + TimeDelta(days=1)
                continue
            
            # 检查小时
            if next_time.hour not in self.hours:
                next_hour = min([h for h in self.hours if h > next_time.hour], default=None)
                if next_hour is not None:
                    next_time = next_time.replace(hour=next_hour, minute=min(self.minutes))
                else:
                    # 跳到下一天的第一个有效小时
                    next_time = next_time.replace(hour=min(self.hours), minute=min(self.minutes)) + TimeDelta(days=1)
                continue
            
            # 检查分钟
            if next_time.minute not in self.minutes:
                next_minute = min([m for m in self.minutes if m > next_time.minute], default=None)
                if next_minute is not None:
                    next_time = next_time.replace(minute=next_minute)
                else:
                    # 跳到下一个小时的第一个有效分钟
                    next_time = next_time.replace(minute=min(self.minutes)) + TimeDelta(hours=1)
                continue
            
            # 所有条件都满足
            return next_time
        
        raise ValueError("unable to find an execution time point.")


class CronTrigger(BaseTrigger):

    def __init__(self, expr, start_time=None, end_time=None):
        self.start_time = start_time
        self.end_time = end_time
        self.cron = CronExpr(expr)
        super().__init__()

    def update(self):
        self.next_run_time = self.cron.get_next_time(from_time=self.start_time)
        if self.next_run_time is None:
            return False
        return True if self.end_time is None else self.next_run_time <= self.end_time


class TriggerFactory(object):

    @staticmethod
    def create(interval=None, datetime=None, cron="", start_time=None, end_time=None):
        if len(list(filter(None, [interval, datetime, cron]))) != 1:
            raise ValueError("can only choose one from [interval, datetime, cron]")
        if isinstance(interval, int) and interval > 0:
            return IntervalTrigger(interval, start_time=start_time, end_time=end_time)
        if isinstance(datetime, (DateTime, )):
            return DateTimeTrigger(datetime)
        elif isinstance(cron, (str, )):
            return CronTrigger(cron, start_time=start_time, end_time=end_time)
        else:
            raise TypeError("can not build trigger object according to params!!!")


class Task(object):

    def __init__(self, title="N/A", target=None, args=(), kwargs=None, sync=True, trigger=None):
        if not callable(target):
            raise TypeError("`target` must be callable")
        if not isinstance(trigger, (IntervalTrigger, DateTimeTrigger, CronTrigger)):
            raise TypeError("`trigger` must be instance of (IntervalTrigger, DateTimeTrigger, CronTrigger)")
        self.title = title
        self.__target = target
        self.__args = args
        self.__kwargs = kwargs or {}
        self.trigger = trigger
        self.sync = sync

    def __str__(self):
        return "{}(title={})".format(type(self).__name__, repr(self.title))

    def __lt__(self, other):
        return self.trigger.next_run_time < other.trigger.next_run_time

    def run(self):
        try:
            if self.sync:
                self.__target(*self.__args, **self.__kwargs)
            else:
                AsyncTask(target=self.__target, args=self.__args, kwargs=self.__kwargs).delay()
        except Exception as e:
            sys.print_exception(e)
            return False
        else:
            return True


class Scheduler(object):

    def __init__(self):
        self.__heap = []
        self.__cond = Condition()
        self.__executor_thread = Thread(target=self.__executor_thread_worker)

    def __task_processing(self):
        task = HeapAlgorithm.pop(self.__heap)
        remaining = task.trigger.get_remaining_seconds()
        if remaining <= 0:
            task.run()
            if task.trigger.update():
                HeapAlgorithm.push(self.__heap, task)
            return
        else:
            HeapAlgorithm.push(self.__heap, task)
            return remaining

    def __executor_thread_worker(self):
        while True:
            with self.__cond:
                try:
                    remaining = self.__task_processing()
                    if remaining is not None:
                        self.__cond.wait(remaining)
                except IndexError:  # heap empty
                    self.__cond.wait()
                except Exception as e:
                    sys.print_exception(e)
                else:
                    pass

    def start(self):
        self.__executor_thread.start()

    def reload(self):
        with self.__cond:
            for task in self.__heap:
                if task.trigger.update():
                    continue
                self.__heap.remove(task)
            HeapAlgorithm.heapify(self.__heap)
            self.__cond.notify()

    def cancel(self, task):
        if not isinstance(task, Task):
            raise TypeError("`task` must be instance of Task")
        with self.__cond:
            if task in self.__heap:
                self.__heap.remove(task)
                HeapAlgorithm.heapify(self.__heap)
                self.__cond.notify()

    def update(self, task, **kwargs):
        if not isinstance(task, Task):
            raise TypeError("`task` must be instance of Task")
        with self.__cond:
            task.trigger = TriggerFactory.create(**kwargs)
            if task.trigger.update() and task in self.__heap:
                HeapAlgorithm.heapify(self.__heap)
                self.__cond.notify()

    def add(self, task):
        if not isinstance(task, Task):
            raise TypeError("`task` must be instance of Task")
        with self.__cond:
            if task in self.__heap:
                raise ValueError("task {} already scheduled".format(task))
            if task.trigger.update():
                HeapAlgorithm.push(self.__heap, task)
                self.__cond.notify()

    def submit(self, title="N/A", target=None, args=(), kwargs=None, sync=True, 
               interval=None, datetime=None, cron="", start_time=None, end_time=None):
        task = Task(
            title=title,
            target=target,
            args=args,
            kwargs=kwargs,
            sync=sync,
            trigger=TriggerFactory.create(
                interval=interval,
                datetime=datetime,
                cron=cron,
                start_time=start_time,
                end_time=end_time
            )
        )
        self.add(task)
        return task

    def task(self, title="N/A", args=(), kwargs=None, sync=True, 
             interval=None, datetime=None, cron="", start_time=None, end_time=None):
        def wrapper(target):
            return self.submit(
                title=title,
                target=target,
                args=args,
                kwargs=kwargs,
                sync=sync,
                interval=interval,
                datetime=datetime,
                cron=cron,
                start_time=start_time,
                end_time=end_time
            )
        return wrapper
